package com.dbflow5.processor.definition;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.Column;
import com.dbflow5.annotation.ColumnMap;
import com.dbflow5.annotation.ModelView;
import com.dbflow5.annotation.ModelViewQuery;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.Validators;
import com.dbflow5.processor.definition.behavior.Behaviors;
import com.dbflow5.processor.definition.behavior.CreationQueryBehavior;
import com.dbflow5.processor.definition.column.ColumnDefinition;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ElementUtility;
import com.dbflow5.processor.utils.JavaPoetExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.List;

/**
 * Description: Used in writing ModelViewAdapters
 */
public class ModelViewDefinition extends EntityDefinition {

    private final ModelView modelView;
    private String queryFieldName = null;

    public ModelViewDefinition(ModelView modelView, ProcessorManager manager, TypeElement element) {
        super(element, manager);
        this.modelView = modelView;
        priority = modelView.priority();
        creationQueryBehavior = new CreationQueryBehavior(modelView.createWithDatabase());

        setOutputClassName("_ViewTable");
    }

    @Override
    public MethodDefinition[] methods() {
        return new MethodDefinition[]{
                new Methods.LoadFromCursorMethod(this),
                new Methods.ExistenceMethod(this),
                new Methods.PrimaryConditionMethod(this)
        };
    }

    private CreationQueryBehavior creationQueryBehavior;

    public int priority;

    @Override
    public Behaviors.AssociationalBehavior associationalBehavior() {
        return new Behaviors.AssociationalBehavior(
                StringUtils.isNullOrEmpty(modelView.name())? modelClassName() : modelView.name(),
                ProcessorUtils.extractTypeNameFromAnnotation(modelView, e -> null, it -> {
                    it.database();
                    return null;
                }),
               modelView.allFields());
    }

    @Override
    public Behaviors.CursorHandlingBehavior cursorHandlingBehavior() {
        return new Behaviors.CursorHandlingBehavior(modelView.orderedCursorLookUp(), modelView.assignDefaultValuesFromCursor());
    }

    @Override
    public void prepareForWriteInternal() {
        queryFieldName = null;
        if(typeElement != null) {
            createColumnDefinitions(typeElement);
        }
    }

    @Override
    public void createColumnDefinitions(TypeElement typeElement) {
        BasicColumnGenerator columnGenerator = new BasicColumnGenerator(manager);
        List<Element> variableElements = ElementUtility.getAllElements(typeElement, manager);
        for (Element element : variableElements) {
            classElementLookUpMap.put(element.getSimpleName().toString(), element);
        }

        Validators.ColumnValidator columnValidator = new Validators.ColumnValidator();
        for (Element variableElement : variableElements) {
            boolean isValidAllFields = ElementUtility.isValidAllFields(associationalBehavior().allFields, variableElement);
            boolean isColumnMap = variableElement.getAnnotation(ColumnMap.class) != null;

            if (variableElement.getAnnotation(Column.class) != null || isValidAllFields || isColumnMap) {
                // package private, will generate helper
                boolean isPackagePrivate = ElementUtility.isPackagePrivate(variableElement);
                ColumnDefinition columnDefinition = columnGenerator.generate(variableElement, this);
                if(columnDefinition != null) {
                    if (columnValidator.validate(manager, columnDefinition)) {
                        columnDefinitions.add(columnDefinition);
                        if (isPackagePrivate) {
                            packagePrivateList.add(columnDefinition);
                        }
                    }

                    if (columnDefinition.type.isPrimaryField()) {
                        manager.logError("ModelView "+elementName+" cannot have primary keys");
                    }
                }
            } else if (variableElement.getAnnotation(ModelViewQuery.class) != null) {
                if (!StringUtils.isNullOrEmpty(queryFieldName)) {
                    manager.logError("Found duplicate queryField name: "+queryFieldName+" for " + elementClassName);
                }

                ExecutableElement element = (ExecutableElement)ElementExtensions.toTypeErasedElement(variableElement, ProcessorManager.manager);
                if (element != null) {
                    TypeElement returnElement = ElementExtensions.toTypeElement(element.getReturnType(), ProcessorManager.manager);
                    ProcessorUtils.ensureVisibleStatic(element, typeElement, "ModelViewQuery");
                    if (!ProcessorUtils.implementsClass(returnElement, manager.processingEnvironment, com.dbflow5.processor.ClassNames.QUERY)) {
                        manager.logError("The function "+variableElement.getSimpleName()+" must return "+com.dbflow5.processor.ClassNames.QUERY+" from " + elementName);
                    }
                }

                queryFieldName = variableElement.getSimpleName().toString();
            }
        }

        if (StringUtils.isNullOrEmpty(queryFieldName)) {
            manager.logError(elementClassName + " is missing the @ModelViewQuery field.");
        }
    }

    @Override
    public List<ColumnDefinition> primaryColumnDefinitions() {
        return columnDefinitions;
    }

    @Override
    public TypeName extendsClass() {
        return ParameterizedTypeName.get(com.dbflow5.processor.ClassNames.MODEL_VIEW_ADAPTER, elementClassName);
    }

    @Override
    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {
        FieldSpec.Builder fieldBuilder = FieldSpec.builder(String.class, "VIEW_NAME");
        fieldBuilder.initializer(CodeBlock.builder().add("\""+associationalBehavior().name+"\"").build());
        typeBuilder.addField(fieldBuilder.build());

        if(elementClassName != null) {
            columnDefinitions.forEach(columnDefinition -> columnDefinition.addPropertyDefinition(typeBuilder, elementClassName));
        }

        this.writeConstructor(typeBuilder);

        writeGetModelClass(typeBuilder, elementClassName);

        creationQueryBehavior.addToType(typeBuilder);

        MethodSpec.Builder methodBuilder =  MethodSpec.methodBuilder("getCreationQuery")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .returns(String.class)
                .addAnnotation(Override.class)
                .addStatement("return " + "\"CREATE VIEW IF NOT EXISTS "+StringUtils.quoteIfNeeded(associationalBehavior().name)+" AS \" + $T.$L().getQuery()", elementClassName, queryFieldName);
        typeBuilder.addMethod(methodBuilder.build());
        associationalBehavior().writeName(typeBuilder);

        JavaPoetExtensions.overrideFun(typeBuilder, ClassNames.OBJECT_TYPE, "getType", builder -> {
            builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            builder.addStatement("return " + "$T.View", ClassNames.OBJECT_TYPE);
            return null;
        });

        MethodDefinition[] methodDefinitionArray = methods();
        for(MethodDefinition definition : methodDefinitionArray) {
            if(definition != null && definition.methodSpec() != null) {
                typeBuilder.addMethod(definition.methodSpec());
            }
        }
    }
}